﻿using System;
using System.Collections.Generic;
using System.Linq;

namespace Perceptron.Engine
{
    public class Perceptron
    {
        private const double Eps = 0.01;
        private const int InputLayerSize = 256;
        private const int HideLayerSize = 128;
        private const int OutputLayerSize = 3;

        private readonly double[,] _hideMatrix  = new double[InputLayerSize, HideLayerSize]; // v[n,h]
        private readonly double[,] _outputMatrix = new double[HideLayerSize, OutputLayerSize]; // w[h,m]
        private readonly double[] _q = new double[HideLayerSize]; // threshold for hide layer Q[h]
        private readonly double[] _t = new double[OutputLayerSize]; // threshold for output layer T[m]
        private readonly double[] _d = new double[OutputLayerSize];

        private const double Alpha = 1;
        private double _betta = 1;
        
        public void Init()
        {
            var rand = new Random();
            for (int i = 0; i < _q.Length; ++i)
                _q[i] = rand.Next(-10, 10)/10.0;
            for (int i = 0; i < _t.Length; ++i)
                _t[i] = rand.Next(-10, 10)/10.0;
            for (int i = 0; i < InputLayerSize; ++i)
                for (int j = 0; j < HideLayerSize; ++j)
                    _hideMatrix[i, j] = rand.Next(-10, 10)/10.0;
            for (int i = 0; i < HideLayerSize; ++i)
                for (int j = 0; j < OutputLayerSize; ++j)
                    _outputMatrix[i, j] = rand.Next(-10, 10)/10.0;
        }

        public void Teach(IEnumerable<ClassImage> objects)
        {
            int idx = 1;
            do
            {
                TeachInternal(objects);
                idx++;
                _betta = 1.0/idx;
            }
            while (Math.Abs(_d.Max()) > Eps);
        }

        private void TeachInternal(IEnumerable<ClassImage> objects)
        {
            foreach(var item in objects)
            {
                var _y = new double[OutputLayerSize];
                var _e = new double[HideLayerSize];
                double[] _g = new double[HideLayerSize];
                var standartOutput = new byte[OutputLayerSize];
                standartOutput[item.ClassId] = 1;

                for (int j = 0; j < HideLayerSize; ++j)
                {
                    _g[j] = _q[j];
                    for (int i = 0; i < InputLayerSize; ++i)
                        _g[j] += _hideMatrix[i, j]*item.Object.Bytes[i];

                    _g[j] = GetSygmoidValue(_g[j]);
                }
                for (int k = 0; k < OutputLayerSize; ++k)
                {
                    _y[k] = _t[k];
                    for (int j = 0; j < HideLayerSize; ++j)
                        _y[k] += _outputMatrix[j, k]*_g[j];

                    _y[k] = GetSygmoidValue(_y[k]);
                    _d[k] = standartOutput[k] - _y[k];
                    for (int j = 0; j < HideLayerSize; ++j )
                        _e[j] += _d[k] * _y[k] * (1 - _y[k]) * _outputMatrix[j,k];           
                }

                for (int j = 0; j < HideLayerSize; ++j)
                {
                    _q[j] += _betta*_g[j]*(1 - _g[j])*_e[j];
                    for (int i = 0; i < InputLayerSize; ++i)
                        _hideMatrix[i, j] += _betta*_g[j]*(1 - _g[j])*_e[j]*item.Object.Bytes[i];
                }
                for (int k = 0; k < OutputLayerSize; ++k)
                {
                    _t[k] += Alpha*_y[k]*(1 - _y[k])*_d[k];
                    for (int j = 0; j < HideLayerSize; ++j)
                        _outputMatrix[j, k] += Alpha*_y[k]*(1 - _y[k])*_d[k]*_g[j];
                }
            }
        }

        public double[] Classificate(byte[] obj)
        {
            var g = new double[HideLayerSize];
            var y = new double[OutputLayerSize];
            for (int j = 0; j < HideLayerSize; ++j)
            {
                g[j] = _q[j];
                for (int i = 0; i < InputLayerSize; ++i)
                    g[j] += _hideMatrix[i, j] * obj[i];
                
                g[j] = GetSygmoidValue(g[j]);
            }
            for (int k = 0; k < OutputLayerSize; ++k)
            {
                y[k] = _t[k];
                for (int j = 0; j < HideLayerSize; ++j)
                    y[k] += _outputMatrix[j, k] * g[j];

                y[k] = GetSygmoidValue(y[k]);
            }
            return y;
        }

        private double GetSygmoidValue(double value)
        {
            return 1/(1 + Math.Pow(Math.E, -value));
        }
    }
}
